#include <fstream>
#include <algorithm>
#include <cstring>
#include "BSPacker.h"

#ifdef _WIN32
#include <direct.h>
#include "dirent.h"
#else
#include <sys/stat.h>
#include <dirent.h>
#endif

#ifndef min
#define min(a, b) ((a)<(b)?(a):(b))
#endif

inline void normpath(std::string &str) {
    std::replace(str.begin(), str.end(), '\\', '/');
}

BSPacker::~BSPacker() {
    for (auto *entry : m_entries) {
        free(entry);
    }
    m_entries.clear();
    m_entriesMap.clear();
}

BSPacker *BSPacker::create(std::string path) {
    normpath(path);

    return new BSPacker(path, true);
}

BSPacker *BSPacker::load(std::string path) {
    normpath(path);

    return new BSPacker(path, false);
}

bool BSPacker::add(std::string filename, std::string entryName) {
    normpath(filename);
    normpath(entryName);

    if (!m_isCreate) {
        std::cout << "ERROR! You can't add a file in load mode\n";
        return false;
    }

    if (m_hasWriteOut) {
        std::cout << "ERROR! You can't add a entry cause it already write out\n";
        return false;
    }

    uint16_t nameSize = entryName.length();
    uint16_t headerSize = sizeof(BSPackEntry) + nameSize;
    auto *entry = (BSPackEntry *) malloc(headerSize);
    entry->headerSize = headerSize;
    entry->nameSize = nameSize;
    memcpy(entry->name, entryName.c_str(), nameSize);
    entry->offset = m_lastOffset + m_lastSize;
    entry->size = fileSize(filename);

    m_lastOffset = entry->offset;
    m_lastSize = entry->size;

    m_totalHeaderSize += headerSize;

    m_entries.push_back(entry);
    m_files.push_back(filename);
    return true;
}

BSPackEntry *BSPacker::get(std::string entryName) {
    normpath(entryName);

    if (m_isCreate) {
        std::cout << "ERROR! You can't get a entry in creation mode\n";
        return nullptr;
    }

    if (m_entriesMap.find(entryName) == m_entriesMap.end()) {
        return nullptr;
    }

    return m_entriesMap[entryName];
}

char *BSPacker::read(std::string entryName) {
    normpath(entryName);

    if (m_isCreate) {
        std::cout << "ERROR! You can't read a entry in creation mode\n";
        return nullptr;
    }

    if (m_entriesMap.find(entryName) == m_entriesMap.end()) {
        std::cout << "ERROR! Entry " << entryName << " not found\n";
        return nullptr;
    }

    std::ifstream in(m_path, std::ifstream::binary | std::ifstream::in);
    if (!in.is_open()) {
        std::cout << "ERROR! Can't open " << m_path << " for read\n";
        return nullptr;
    }

    auto *entry = m_entriesMap[entryName];

    char *data = (char *) malloc(entry->size);

    in.seekg(entry->offset, std::ifstream::beg);
    in.read(data, entry->size);
    return data;
}

BSPacker::BSPacker(const std::string &path, bool isCreate)
        : m_path(path),
          m_isCreate(isCreate),
          m_hasWriteOut(false),
          m_lastOffset(0),
          m_lastSize(0),
          m_totalHeaderSize(0) {

    if (!m_isCreate) {
        if (!loadFile(path)) {
            for (auto *entry : m_entries) {
                free(entry);
            }
            m_entries.clear();
        }
    }
}

bool BSPacker::pack(std::string dirname) {
    normpath(dirname);

    if (!m_isCreate) {
        std::cout << "ERROR! You can't pack in load mode\n";
        return false;
    }

    auto fileList = listFiles(dirname);
    for (auto &file : fileList) {
        add(dirname + file, file.substr(1));
    }
    writeOut();
    return true;
}

bool BSPacker::writeOut() {
    if (m_hasWriteOut) {
        std::cout << "ERROR! You can't write out twice\n";
        return false;
    }
    m_hasWriteOut = true;

    BSPackEntry emptyEntry{0};
    m_totalHeaderSize += sizeof(BSPackEntry);


    std::ofstream out(m_path, std::ofstream::binary | std::ofstream::out);
    if (!out.is_open()) {
        std::cout << "ERROR! Can't open " << m_path << " for write\n";
        return false;
    }

    for (auto *entry : m_entries) {
        entry->offset += m_totalHeaderSize;
        out.write((const char *) entry, entry->headerSize);
        free(entry);
    }
    m_entries.clear();

    // write a empty entry
    out.write((const char *) &emptyEntry, sizeof(BSPackEntry));

    for (auto &filename : m_files) {
        std::ifstream in(filename, std::ifstream::binary | std::ifstream::in);
        if (!in.is_open()) {
            std::cout << "ERROR! Can't open " << filename << " for read\n";
            return false;
        }

        out << in.rdbuf();
        in.close();
    }
    m_files.clear();

    out.close();
    return true;
}

bool BSPacker::loadFile(const std::string &path) {
    std::ifstream in(path, std::ifstream::binary | std::ifstream::in);
    if (!in.is_open()) {
        std::cout << "ERROR! Can't open " << path << " for read\n";
        return false;
    }

    while (!in.eof()) {
        uint16_t headerSize;
        in.read((char *) &headerSize, sizeof(uint16_t));
        if (headerSize == 0) break;

        auto *entry = (BSPackEntry *) malloc(headerSize);
        entry->headerSize = headerSize;
        in.read((char *) &entry->nameSize, headerSize - sizeof(uint16_t));

//        std::cout << "found entry " << std::string(entry->name, entry->nameSize) << "\n";
        m_entries.push_back(entry);
        m_entriesMap[std::string(entry->name, entry->nameSize)] = entry;
    }

    in.close();

    return true;
}

bool BSPacker::unpack(std::string dirname) {
    normpath(dirname);

    if (m_isCreate) {
        std::cout << "ERROR! You can't unpack in creation mode\n";
        return false;
    }

    std::ifstream in(m_path, std::ifstream::binary | std::ifstream::in);
    if (!in.is_open()) {
        std::cout << "ERROR! Can't open " << m_path << " for read\n";
        return false;
    }

    for (auto *entry : m_entries) {
        std::string filename = dirname + "/" + std::string(entry->name, entry->nameSize);

        std::string d = getDirname(filename);
        if (!ensureDir(d)) {
            std::cout << "ERROR! Failed mkdir " << d << "\n";
            return false;
        }

        std::ofstream out(filename, std::ofstream::binary | std::ofstream::out);
        if (!out.is_open()) {
            std::cout << "ERROR! Can't open " << filename << " for write\n";
            return false;
        }

//        std::cout << "read " << entry->offset << ", " << entry->size << " "
//                  << std::string(entry->name, entry->nameSize) << "\n";

        in.seekg(entry->offset, std::ifstream::beg);

        char buffer[1024];
        uint32_t readSize = 0;

        while (readSize < entry->size) {
            in.read(buffer, min(sizeof(buffer), entry->size - readSize));
            uint32_t s = in.gcount();
            out.write(buffer, s);
            readSize += s;
        }

        out.close();

    }

    in.close();

    return true;
}

std::string BSPacker::getDirname(const std::string &filename) {
    size_t pos = filename.find_last_of("/\\");
    return filename.substr(0, pos);
}

bool BSPacker::dirExists(const std::string &dirname) {
    DIR *dir = opendir(dirname.c_str());
    bool exists = false;
    if (dir) {
        exists = true;
        closedir(dir);
    }
    return exists;
}

bool BSPacker::ensureDir(const std::string &dirname) {
    if (dirExists(dirname))
        return true;

    std::string parent = getDirname(dirname);
    if (parent != dirname && !dirExists(parent)) {
        bool suc = ensureDir(parent);
        if (!suc)
            return false;
    }
    int error = 0;
#ifdef _WIN32
    error = ::_mkdir(dirname.c_str());
#else
    mode_t mode = 0755;
    error = ::mkdir(dirname.c_str(), mode);
#endif
    return error == 0;
}

std::vector<std::string> BSPacker::listFiles(const std::string &dirname, const std::string &basename) {
    std::vector<std::string> fileList;
    struct dirent **files;
    int n = scandir(dirname.c_str(), &files, nullptr, alphasort);
    if (n >= 0) {
        for (int i = 0; i < n; i++) {
            struct dirent *ent;
            ent = files[i];
            if (std::string(ent->d_name) == "."
                || std::string(ent->d_name) == "..") {
                continue;
            }
            switch (ent->d_type) {
                case DT_REG: {
//                    std::cout << "add  " << (basename + "/" + ent->d_name) << "\n";
                    fileList.emplace_back(basename + "/" + ent->d_name);
                    break;
                }
                case DT_DIR: {
                    std::vector<std::string> subList = listFiles(dirname + "/" + ent->d_name,
                                                                 basename + "/" + ent->d_name);
                    for (auto &f : subList) {
                        fileList.push_back(f);
                    }
                    break;
                }
                default: {
//                    std::cout << "skip " << (basename + "/" + ent->d_name) << ", file type: " << ent->d_type << "\n";
                    break;
                }
            }
            free(files[i]);
        }
        free(files);
    } else {
        std::cout << "ERROR! Can't open dir " << dirname << " " << strerror(errno) << "\n";
    }

    return fileList;
}

uint32_t BSPacker::fileSize(const std::string &filename) {
    std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary);
    return in.tellg();
}
